package gin

import (
	"bytes"
	"fmt"
	"log"
	"net/http"
	"os"
	"runtime"
)

var (
	// 未知
	dunno = []byte("???")
	// 中心点
	centerDot = []byte("·")
	// 点
	dot = []byte(".")
	// 斜杠
	slash = []byte("/")
)

// stack 用于生成当前程序调用栈的详细信息,方便进行调试和错误解析
func stack(skip int) []byte {

	// 创建一个新的缓冲区,用于存储生成的调用栈信息
	buf := new(bytes.Buffer)

	// 用于存储当前加载文件的所有行
	var lines [][]byte
	// 记录上一次加载的文件路径
	var lastFile string

	// 从skip指定的帧数开始遍历调用栈

	for i := skip; ; i++ {
		// 获取当前调用帧的程序计数器（pc）,文件名，行号和是否成功获取信息的标准
		pc, file, line, ok := runtime.Caller(i)

		// 如果无法获取更多调用帧信息，退出循环
		if !ok {
			break
		}
		// 格式化并输出当前调用帧的文件名、行号和程序计数器值
		fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)

		// 如果当前文件与上一次加载的文件不同
		if file != lastFile {
			// 读取当前文件的内容
			data, err := os.ReadFile(file)

			// 如果读取文件时出错，跳过当前文件，继续下一个调用帧
			if err != nil {
				continue
			}
			// 将文件内容按行分割，存储在lines切片中
			lines = bytes.Split(data, []byte{'\n'})
			// 更新lastFile为当前文件路径
			lastFile = file
		}
		// 格式化并输出当前调用帧对应的函数名和源代码行内容
		fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line))
	}
	// 将缓冲区的的内容以字节切片的形式返回
	return buf.Bytes()
}

// source 返回第n行的去除空白的切片
func source(lines [][]byte, n int) []byte {
	// 在堆栈跟踪中，行是1索引的，但我们的数组是0索引的
	if n < 0 || n >= len(lines) {
		return dunno
	}

	// 移除指定行的首位空白字符
	return bytes.TrimSpace(lines[n])
}

// function 如果可能,返回包含pc的函数名
func function(pc uintptr) []byte {
	fn := runtime.FuncForPC(pc)
	if fn == nil {
		return dunno
	}
	// 获取调用者的函数名
	name := []byte(fn.Name())

	// 去除函数的包名
	// 也就是我们看到runtime/debug.*T.ptrmethod 我们想要*T.ptrmethod
	// 包路径也可能包含点例如(code.google.com/...),所以首先消除路径前缀

	// 获取最后一次 "/" 出现的下标
	if lastSlash := bytes.LastIndex(name, slash); lastSlash >= 0 {
		// 截取 "/" 后面的字符串
		name = name[lastSlash+1:]
	}
	// 获取首次出现的 "." 的下标,也就是包名点
	if period := bytes.Index(name, dot); period >= 0 {
		// 截取 "." 后面的字符串
		name = name[period+1:]
	}

	// -1代表替换次数(表示不限次数),将所有的中 "·"替换成 "."
	name = bytes.Replace(name, centerDot, dot, -1)

	return name
}

// Recovery 返回一个从任何恐慌中恢复的中间件,如果有,则写入500
// 当Martini处理开发模式时,Recovery将打印恐慌信息,而不是写入500
func Recovery() HandlerFunc {
	return func(c *Context) {
		fmt.Println("执行了Recovery中间件")

		// 打印错误列表中的错误信息,并捕获panic错误
		defer func() {
			// 如果错误列表总有信息,则打印这些错误
			if len(c.Errors) > 0 {
				log.Println(c.Errors)
			}
			// 捕获错误,并自定义处理
			if err := recover(); err != nil {
				stack := stack(3)
				log.Printf("PANIC: %s\n%s", err, stack)
				c.Writer.WriteHeader(http.StatusInternalServerError)
			}
		}()
		// 由该中间件调用handlers中的处理函数,如果出现错误就正好被捕获
		// 并且由于Context维护着一个index负责计数因此不用担心重复执行的问题
		// 并且由于后续的内部链都是由Recovery内部执行,因此出现的错误都会被其捕获到
		c.Next()
	}
}
